Skip to content

feat(watchlist): implement Plex watchlist GraphQL API support#3008

Open
xNul wants to merge 6 commits into
seerr-team:developfrom
xNul:watchlist-gql-grab
Open

feat(watchlist): implement Plex watchlist GraphQL API support#3008
xNul wants to merge 6 commits into
seerr-team:developfrom
xNul:watchlist-gql-grab

Conversation

@xNul
Copy link
Copy Markdown

@xNul xNul commented May 10, 2026

Description

This PR builds off of #1426 and aims to close #1378.

Currently, Seerr's Plex watchlist sync functionality requires each Plex user's individual user token. This is a burden on the user because it means they have to get the login credentials for every Plex user's account or have the user access Seerr and login themselves. For situations where obtaining a user's token isn't possible or there is no desire to give access to Seerr's web interface, Seerr hasn't been able to do a watchlist sync.

However, this is possible to do with Plex's GraphQL API. The only requirement to access a user's watchlist via the GraphQL API is that the user's watchlist is accessible to the user token which is making the API call. For example, if a user makes their watchlist public, it can be synced. If a user is friends with the user token which is making the API call, it can be synced.

Since the owner of a Seerr instance is the owner of the Plex media server, the Plex media server owner can add their users as friends (an easier task, if they aren't already friends) and have access to all of the watchlists of the media server. This means, there is no need to have each Plex user login to Seerr in order to provide Plex watchlist sync functionality.

Originally, PR #1426 was closed by the author with the expectation Plex was about to provide a public API which would officially solve this issue. It has been over a year now and as far as I can tell, no such API has been released so I have brought the PR back.

Progress Checklist

  • Rebased and modified for the latest commit
  • Removed legacy watchlist sync API
  • Decoupled getWatchlist from API token
  • Robust way to fetch and store UUIDs
  • Restored watchlist caching functionality
  • Database migrations for the new UUID column
  • Handles database migration edge cases where UUID values may be null
  • Fixed tests
  • Added an access check when enabling watchlist sync for a user
  • Handle pagination differences
  • Simplify SQLite migration code?
  • Better getWatchlist error handling?

Important Changes

  • A new plexUuid column has been added to the User entity/table.
  • The legacy watchlist sync API has been removed because it was redundant. All previous functionality has been retained.
  • A new Plex API /api/home/users is used to provide all the Plex UUIDs and replaces the /api/users Plex API call during /import-from-plex Plex user import.

The Pagination Issue

Pagination in Seerr is built around offsets, but the GraphQL watchlist API uses cursors. More info here. We need a way to bridge the gap and I'm not sure which way to go. I have marked the respective areas where pagination needs to be fixed with TODO comments.

The page is set via the page query parameter

const page = req.query.page ? Number(req.query.page) : 1;

which can be set 1-n at any point in time. With offset-based pagination, that's a simple operation that only returns page_size results for a given page, but cursor-based pagination would require consuming page_size*page results to find the results for a given page. Page caching should help here though.

Edit: The cursor for each watchlist item is simply when the item was added in unix epoch time encoded in base64. We can do a lot with this knowledge.

Edit 2: I have reverse engineered the GraphQL fields for GetWatchlistHub. There may still be more, but I don't think there are any left that are related to pagination.

Click to expand GetWatchlistHub example query
fragment imageItems on MetadataItemImageSet {
  coverArt
  coverPoster
  thumbnail
  art
  __typename
}

fragment userStateItems on UserState {
  viewCount
  viewedLeafCount
  watchlistedAt
  __typename
}

fragment parentFields on MetadataItem {
  id
  title
  type
  index
  key
  publishedAt
  __typename

  images {
    ...imageItems
  }

  userState {
    ...userStateItems
  }
}

fragment itemFields on MetadataItem {
  id
  title
  type
  year
  guid
  key
  slug

  duration
  index
  publishedAt
  originallyAvailableAt
  publicPagesURL

  leafCount
  childCount

  tagline
  imdbRatingCount

  __typename

  parent {
    ...parentFields
  }

  grandparent {
    ...parentFields
  }

  userState @skip(if: $skipUserState) {
    ...userStateItems
  }

  images {
    ...imageItems
  }
}

# `first` and `last` can't be used at the same time. One must be removed.
query GetWatchlistHub(
  $uuid: ID = ""
  $first: PaginationInt!
  $after: String
  $last: PaginationInt!
  $before: String
  $skipUserState: Boolean = false
) {
  user(id: $uuid) {
    watchlist(
      first: $first
      after: $after
      last: $last
      before: $before
    ) {
      nodes {
        ...itemFields
      }

      pageInfo {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
    }
  }
}

How Has This Been Tested?

  • Passes pnpm test
  • Manual testing of each edge case
    • Updating an old database to a new UUID database
    • Plex watchlists with no items
    • Plex watchlists with items
    • Plex watchlists without access
    • Plex watchlists with access
    • Plex watchlists with access -> set to private -> without access auto-detection and configuration
    • Watchlist access check trigger before migration UUID column population
    • Login and watchlist sync with another Plex user
    • Running from scratch with no existing config or database
  • I have been running it myself for a small set of users

Screenshots / Logs (if applicable)

Example attempt to enable plex watchlist sync without access to it

image

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

AI Disclosure: The equivalent of Googling. AI was not used to write any of this code.

Summary by CodeRabbit

  • New Features

    • Added Plex home users support and migrated watchlist sync to UUID-based identification with cursor-style pagination; backend now persists Plex UUIDs and seeds test accounts with UUIDs.
  • Bug Fixes

    • Improved handling when a Plex watchlist is inaccessible: clearer error, user-facing toast, and automatic disabling of watchlist sync when access fails.
  • Documentation

    • Updated Plex Watchlist Auto Request docs to clarify access prerequisites and failure guidance.

Review Change Stack

xNul added 4 commits May 10, 2026 01:32
Previous Plex Watchlist Sync was limited to those who logged into Seerr with their Plex accounts.
GraphQL-based Plex Watchlist Sync expands sync to all who are friends with those who logged into
Seerr and unrestricted/public Plex Watchlists. Oftentimes, just importing a Plex user is enough to
allow Plex Watchlist Sync.

BREAKING CHANGE: Addition of a new plexUuid column to the User table in the database.
GraphQL-based Plex Watchlist Sync requires usage of Plex UUIDs. Migration from older versions would
result in a new User.plexUuid column, but filled with NULL values. Now, migration from older
versions will automatically populate the User.plexUuid column where applicable.
Plex Watchlist Sync for a user may result in an access denied error. As a preventative measure and
to minimize GQL requests, a check is now performed on the enabling of Plex Watchlist Sync for a
user to validate access. Additionally, if access to a user's Plex Watchlist is denied in the course
of normal operations, the user's Plex Watchlist Sync settings will be disabled.
@xNul xNul requested a review from a team as a code owner May 10, 2026 04:36
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

📝 Walkthrough

Walkthrough

This PR enables Plex watchlist fetching via GraphQL and Plex home user UUIDs, eliminating the requirement for individual user Plex tokens. New API methods retrieve home users and GraphQL-backed watchlists. Database migrations add plexUuid to the User entity. Sync logic implements UUID-based fetches with fallback token logic and access validation. Routes are updated to use UUIDs. UI error handling added for watchlist access denial.

Changes

Plex UUID-Based Watchlist via GraphQL

Layer / File(s) Summary
Data Contracts & Types
server/api/plextv.ts, server/constants/error.ts, server/entity/User.ts
Introduces HomeUsersResponse interface for Plex user data, replaces watchlist response typing with GraphQL WatchlistResponse structure containing data.user.watchlist.nodes and pageInfo, adds WatchlistAccessDenied error code, and adds nullable plexUuid column to User with select: false.
Plex API Methods
server/api/plextv.ts
Implements getHomeUsers() calling /api/home/users; rewrites getWatchlist() to accept uuid and pagination params (first, after), POST GraphQL query to Plex community endpoint with UUID-based caching, fetch metadata via getRolling per node, and return pagination object with endCursor and totalSize.
Database Migrations
server/migration/postgres/*, server/migration/sqlite/*
PostgreSQL migration adds nullable plexUuid varchar column via ALTER TABLE; SQLite migration rebuilds user_push_subscription, blocklist, and user tables via temporary table pattern to introduce plexUuid column while preserving existing constraints and data.
Watchlist Sync Logic
server/lib/watchlistsync.ts
Updates syncWatchlist() to load the main user, repair legacy PLEX users by calling getHomeUsers() and matching by Plex ID, then query all PLEX users with plexUuid and settings; syncUserWatchlist() now accepts mainPlexToken for fallback, calls getWatchlist(uuid), and disables sync settings early if watchlist data is inaccessible (endCursor === undefined).
Plex User Onboarding
server/routes/auth.ts
Sets plexUuid from Plex OAuth account payload when creating new Plex users during initial sign-in and newPlexLogin flow.
Route Handler Updates
server/routes/discover.ts, server/routes/user/index.ts, server/routes/user/usersettings.ts
Updates /import-from-plex to use getHomeUsers() and assign plexUuid to imported users; updates /watchlist endpoints to require plexUuid and call getWatchlist(uuid) instead of offset-based fetch; adds watchlist sync enablement logic in usersettings that verifies UUID availability, derives it via home users if missing, and rejects with WatchlistAccessDenied if watchlist access fails.
UI Messages, Tests & Docs
src/components/.../UserGeneralSettings/index.tsx, server/utils/seedTestDb.ts, docs/using-seerr/plex/watchlist-auto-request.md, src/i18n/locale/en.json
Adds i18n key and dedicated toast handling for WatchlistAccessDenied; seeds test DB users with fixed plexUuid values; updates documentation prerequisites and warning message.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • fallenbagel
  • 0xSysR3ll
  • gauthier-th

Poem

🐰 I hopped through Plex with a tiny clue,
UUIDs in pockets, fetching watchlists anew,
GraphQL threads friends' lists through the night,
Tokens fall back — everything's right,
Hop, sync, and toast — the rabbit winks in delight.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing Plex watchlist GraphQL API support, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from #1378: fetches Plex watchlists via GraphQL API without requiring user tokens, uses admin token via getHomeUsers/getWatchlist, stores Plex UUIDs, validates watchlist access, and enables seamless watchlist sync for imported users.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the watchlist GraphQL implementation objective. Database migrations, User entity updates, API routes, and UI changes all support the core feature without introducing unrelated modifications.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
server/routes/auth.ts (1)

140-147: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist plexUuid when updating an existing Plex-linked user.

On Line 141+, the login refresh updates Plex fields but skips plexUuid. Users upgraded from older data can stay plexUuid = null, which breaks UUID-based watchlist flows.

Suggested patch
           user.plexToken = body.authToken;
           user.plexId = account.id;
+          user.plexUuid = account.uuid ?? user.plexUuid;
           user.avatar = account.thumb;
           user.email = account.email;
           user.plexUsername = account.username;
           user.userType = UserType.PLEX;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/auth.ts` around lines 140 - 147, The update block that
refreshes Plex-linked user fields sets plexToken, plexId, avatar, email,
plexUsername and userType but omits persisting plexUuid; set user.plexUuid =
account.uuid (or the correct UUID property returned on the Plex account object)
before calling userRepository.save(user) so legacy users get their plexUuid
populated and saved. Ensure you reference the existing variables user, account
and the save call (userRepository.save) in the same update flow.
server/routes/discover.ts (1)

927-929: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Pagination contract is currently broken for Plex watchlist.

Line 927 computes offset pagination, but Line 978 ignores it and always fetches by UUID without cursor input. page > 1 can therefore return incorrect/duplicate results.

Also applies to: 977-983

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/discover.ts` around lines 927 - 929, The pagination logic
computes page and offset (page, offset, itemsPerPage) but the watchlist fetch
path still ignores offset and always fetches by UUID, causing duplicates for
page > 1; update the watchlist retrieval (the call around the code that fetches
by UUID in this route handler) to accept pagination arguments (offset/limit or a
cursor) and pass itemsPerPage and offset (or page) through to the data access
method (e.g., modify getWatchlistItemsByUUID or replace it with a paginated
method) so that results are limited and offset correctly for pages > 1; ensure
the same fix is applied to the other occurrence referenced (the block around
lines 977–983) so both branches honor the computed offset and itemsPerPage.
server/routes/user/index.ts (1)

687-698: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update plexUuid for already-existing users during Plex import.

On Line 687+, existing users are refreshed with Plex profile data, but plexUuid is never updated. This leaves legacy users without the UUID required by watchlist fetches.

Suggested patch
           if (user) {
             // Update the user's avatar with their Plex thumbnail, in case it changed
             user.avatar = account.thumb;
             user.email = account.email;
             user.plexUsername = account.username;
+            user.plexUuid = account.uuid ?? user.plexUuid;

             // In case the user was previously a local account
             if (user.userType === UserType.LOCAL) {
               user.userType = UserType.PLEX;
               user.plexId = parseInt(account.id);
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/user/index.ts` around lines 687 - 698, When updating an
existing user in the if (user) block in server/routes/user/index.ts, also set
the user's plexUuid from the Plex account payload before saving; e.g. assign
user.plexUuid = account.uuid || account.id (or account.uuid if present)
alongside the existing lines that set avatar/email/plexUsername and plexId, then
call userRepository.save(user) as before so legacy users receive the required
UUID for watchlist fetches.
server/api/plextv.ts (2)

434-445: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't collapse every Plex failure into the access-denied sentinel.

The catch block always returns endCursor: undefined, which is the same signal the callers use for an inaccessible watchlist. That means a transient timeout/5xx/schema issue can surface as WatchlistAccessDenied in usersettings.ts, and watchlistsync.ts can disable both sync toggles for a user even though access was never actually denied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/plextv.ts` around lines 434 - 445, The catch block in
server/api/plextv.ts currently maps every error to the access-denied sentinel
(endCursor: undefined); change it so that you only return that sentinel when the
error is an actual access-denied response (e.g., HTTP 401/403 or a Plex auth
error), otherwise do not swallow the error—either rethrow the exception or
return a non-sentinel response so callers can distinguish
transient/server/schema failures; update the catch handling around logger.error
to inspect e.status/e.code (or error message patterns) and only return the
access-denied shape for true auth errors, leaving other errors to propagate.

391-394: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use watchlistItem.id instead of accessing a non-existent property.

WatchlistResponse.data.user.watchlist.nodes is declared as { id: string }[], so watchlistItem.ratingKey does not exist on this type. Use watchlistItem.id in the log message.

Suggested fix
-                `Item with ratingKey ${watchlistItem.ratingKey} returned no metadata, skipping.`,
+                `Item with id ${watchlistItem.id} returned no metadata, skipping.`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/plextv.ts` around lines 391 - 394, The log message in the block
that checks if (!metadata) uses a non-existent property watchlistItem.ratingKey;
change it to use watchlistItem.id instead so the logger.warn call references the
correct property (e.g., `Item with id ${watchlistItem.id} returned no metadata,
skipping.`) within the same `if (!metadata)` block where `watchlistItem` and
`metadata` are checked (the logger.warn call).
🧹 Nitpick comments (1)
server/utils/seedTestDb.ts (1)

37-55: ⚡ Quick win

Use distinct plexUuid values per seeded user.

Both seeded Plex users currently share the same UUID, which can mask UUID-routing/watchlist bugs in tests.

Suggested patch
-  user.plexUuid = 'a63j9ad8';
+  user.plexUuid = 'a63j9ad8-admin';

...
-  otherUser.plexUuid = 'a63j9ad8';
+  otherUser.plexUuid = 'a63j9ad8-friend';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/seedTestDb.ts` around lines 37 - 55, Seeded Plex users share the
same plexUuid which can hide routing/watchlist bugs; update the seeding in
seedTestDb.ts so each user gets a unique plexUuid (e.g., change
otherUser.plexUuid to a different value than user.plexUuid) and ensure any other
seeded users also receive distinct plexUuid values; locate the assignments to
user.plexUuid and otherUser.plexUuid in the file and give them different unique
strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/plextv.ts`:
- Around line 306-317: getWatchlist currently only fetches the first page
(default first=20) and returns page-limited items and totalSize, so long lists
are truncated; update getWatchlist to follow pagination by looping while
pageInfo.hasNextPage is true, calling the same fetch logic with
pageInfo.endCursor/after to request subsequent pages, concatenating
PlexWatchlistItem results into a single items array, update endCursor to the
last page's endCursor, and set totalSize from the server-provided total (if
available) or the final items.length instead of per-page length; apply the same
pagination fix to the other similar blocks referenced (around lines 323-352 and
364-433) to ensure full list retrieval and correct totalSize reporting.

In `@server/lib/watchlistsync.ts`:
- Around line 45-58: The loop only updates users when account.email is truthy,
leaving managed Plex users (no email) with plexUuid null; remove the
account.email guard so every entry from mainPlexTv.getHomeUsers() (iterate
plexHomeUsersResponse.MediaContainer.User and use account.id) attempts to find
the matching user via
userRepository.createQueryBuilder('user').where('user.plexId = :id', { id:
account.id }).getOne(), and if found set user.plexUuid = account.uuid (or
account.uuid only when present) and await userRepository.save(user); this
ensures syncUserWatchlist() can find and sync managed users too.

In `@server/routes/user/usersettings.ts`:
- Around line 148-158: The current backfill logic for user.plexUuid incorrectly
requires account.email to be present before assigning account.uuid, causing
managed/home users without emails to never receive a UUID; update the loop
inside the plexTvApi.getHomeUsers() handling so that you match by
Number(account.id) === user.plexId regardless of account.email and set
user.plexUuid = account.uuid and call userRepository.save(user) when the id
matches (retain the existing break after save); remove the account.email
condition so UUIDs are populated for users without email addresses.

---

Outside diff comments:
In `@server/api/plextv.ts`:
- Around line 434-445: The catch block in server/api/plextv.ts currently maps
every error to the access-denied sentinel (endCursor: undefined); change it so
that you only return that sentinel when the error is an actual access-denied
response (e.g., HTTP 401/403 or a Plex auth error), otherwise do not swallow the
error—either rethrow the exception or return a non-sentinel response so callers
can distinguish transient/server/schema failures; update the catch handling
around logger.error to inspect e.status/e.code (or error message patterns) and
only return the access-denied shape for true auth errors, leaving other errors
to propagate.
- Around line 391-394: The log message in the block that checks if (!metadata)
uses a non-existent property watchlistItem.ratingKey; change it to use
watchlistItem.id instead so the logger.warn call references the correct property
(e.g., `Item with id ${watchlistItem.id} returned no metadata, skipping.`)
within the same `if (!metadata)` block where `watchlistItem` and `metadata` are
checked (the logger.warn call).

In `@server/routes/auth.ts`:
- Around line 140-147: The update block that refreshes Plex-linked user fields
sets plexToken, plexId, avatar, email, plexUsername and userType but omits
persisting plexUuid; set user.plexUuid = account.uuid (or the correct UUID
property returned on the Plex account object) before calling
userRepository.save(user) so legacy users get their plexUuid populated and
saved. Ensure you reference the existing variables user, account and the save
call (userRepository.save) in the same update flow.

In `@server/routes/discover.ts`:
- Around line 927-929: The pagination logic computes page and offset (page,
offset, itemsPerPage) but the watchlist fetch path still ignores offset and
always fetches by UUID, causing duplicates for page > 1; update the watchlist
retrieval (the call around the code that fetches by UUID in this route handler)
to accept pagination arguments (offset/limit or a cursor) and pass itemsPerPage
and offset (or page) through to the data access method (e.g., modify
getWatchlistItemsByUUID or replace it with a paginated method) so that results
are limited and offset correctly for pages > 1; ensure the same fix is applied
to the other occurrence referenced (the block around lines 977–983) so both
branches honor the computed offset and itemsPerPage.

In `@server/routes/user/index.ts`:
- Around line 687-698: When updating an existing user in the if (user) block in
server/routes/user/index.ts, also set the user's plexUuid from the Plex account
payload before saving; e.g. assign user.plexUuid = account.uuid || account.id
(or account.uuid if present) alongside the existing lines that set
avatar/email/plexUsername and plexId, then call userRepository.save(user) as
before so legacy users receive the required UUID for watchlist fetches.

---

Nitpick comments:
In `@server/utils/seedTestDb.ts`:
- Around line 37-55: Seeded Plex users share the same plexUuid which can hide
routing/watchlist bugs; update the seeding in seedTestDb.ts so each user gets a
unique plexUuid (e.g., change otherUser.plexUuid to a different value than
user.plexUuid) and ensure any other seeded users also receive distinct plexUuid
values; locate the assignments to user.plexUuid and otherUser.plexUuid in the
file and give them different unique strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f24d628b-f1a6-4f03-aae7-8d31cfd5bb63

📥 Commits

Reviewing files that changed from the base of the PR and between dfde4d3 and 27cbe73.

📒 Files selected for processing (12)
  • server/api/plextv.ts
  • server/constants/error.ts
  • server/entity/User.ts
  • server/lib/watchlistsync.ts
  • server/migration/postgres/1777262170937-AddPlexUuidToUser.ts
  • server/migration/sqlite/1777247406444-AddPlexUuidToUser.ts
  • server/routes/auth.ts
  • server/routes/discover.ts
  • server/routes/user/index.ts
  • server/routes/user/usersettings.ts
  • server/utils/seedTestDb.ts
  • src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx

Comment thread server/api/plextv.ts
Comment on lines +306 to 317
public async getWatchlist(
uuid: string,
{
first = 20,
after = undefined,
}: { first?: number; after?: number | undefined } = {}
): Promise<{
first: number;
after: number | undefined;
endCursor: string | undefined;
totalSize: number;
items: PlexWatchlistItem[];
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This only processes the first watchlist page.

getWatchlist() fetches one page (first = 20) and never follows pageInfo.hasNextPage/endCursor. Every caller added in this PR uses the default path, so any Plex watchlist with more than 20 entries will be silently truncated during sync/import. totalSize is also derived from the current page length, so callers cannot detect the truncation from the return value.

Also applies to: 323-352, 364-433

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/plextv.ts` around lines 306 - 317, getWatchlist currently only
fetches the first page (default first=20) and returns page-limited items and
totalSize, so long lists are truncated; update getWatchlist to follow pagination
by looping while pageInfo.hasNextPage is true, calling the same fetch logic with
pageInfo.endCursor/after to request subsequent pages, concatenating
PlexWatchlistItem results into a single items array, update endCursor to the
last page's endCursor, and set totalSize from the server-provided total (if
available) or the final items.length instead of per-page length; apply the same
pagination fix to the other similar blocks referenced (around lines 323-352 and
364-433) to ensure full list retrieval and correct totalSize reporting.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known issue and to be done as stated in the PR description.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xNul, understood — thanks for the clarification! I can see the PR description already calls this out as an outstanding item with TODOs marking the relevant spots in the code. I'll defer to that tracking. Would you like me to open a follow-up GitHub issue to track the cursor-pagination implementation for getWatchlist() so it doesn't get lost after this PR merges?

(。•ᴗ•。)🐇

Comment thread server/lib/watchlistsync.ts Outdated
Comment thread server/routes/user/usersettings.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/api/plextv.ts (1)

375-445: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't map generic failures to the access-denied sentinel.

Non-404 metadata errors are rethrown at Lines 382-383, and the outer catch then returns { endCursor: undefined }. The callers in this PR use that shape as “watchlist not accessible”, so a transient Plex/community failure or one bad metadata fetch can incorrectly block watchlist sync for a user who actually has access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/plextv.ts` around lines 375 - 445, The outer catch currently
swallows all errors and returns the "access-denied sentinel" (endCursor:
undefined), which causes transient or unrelated failures to be treated as “no
access”; change the final catch so it only maps explicit access-denied errors
(e.g. e.response?.status === 401 || e.response?.status === 403) to the sentinel
and otherwise rethrows the error (or preserve the cached endCursor via
cachedWatchlist?.response.data?.user.watchlist.pageInfo.endCursor) so transient
failures are not misclassified; update the catch block that currently logs via
logger.error('Failed to retrieve watchlist items', ...) and returns { first,
after, endCursor: undefined, ... } to perform the status check and rethrow for
non-access errors.
🧹 Nitpick comments (1)
docs/using-seerr/plex/watchlist-auto-request.md (1)

63-65: 💤 Low value

Consider improving conciseness and clarity.

Two minor refinements for this warning callout:

  1. The title could be more concise: "Watchlist Inaccessible" instead of "Watchlist Not Accessible"
  2. The phrase "prerequisite #1" could be clearer for users troubleshooting this error. Consider: "you have not satisfied any of the watchlist access options listed in the first prerequisite" or simply "your watchlist is not accessible to Seerr"
📝 Proposed refinement
-:::warning Watchlist Not Accessible
-If you receive the error "User's watchlist is not accessible." when saving your settings at the end of Step 2, you have not fulfilled prerequisite `#1`.
+:::warning Watchlist Inaccessible
+If you receive the error "User's watchlist is not accessible." when saving your settings at the end of Step 2, you have not satisfied any of the watchlist access options listed in the first prerequisite.
 :::
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/using-seerr/plex/watchlist-auto-request.md` around lines 63 - 65, Change
the warning callout title from "Watchlist Not Accessible" to the more concise
"Watchlist Inaccessible" and replace the vague phrase "you have not fulfilled
prerequisite `#1`" with a clearer explanation such as "your watchlist is not
accessible to Seerr" or "you have not satisfied any of the watchlist access
options listed in the first prerequisite" so users immediately understand the
cause and next steps; update the callout content in the watchlist warning block
accordingly (look for the exact strings "Watchlist Not Accessible" and
"prerequisite `#1`" to locate the text).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/plextv.ts`:
- Around line 306-315: The getWatchlist method's after cursor is incorrectly
typed as number; update the getWatchlist signature and its parameter
destructuring to use after: string | undefined (instead of number | undefined)
so it matches the GraphQL $after: String and pageInfo.endCursor string, ensure
any local uses that pass after into the GraphQL variables treat it as a string,
and update any callers/tests to stop casting cursors to number (or to pass
strings) so the internal API contract is consistent; reference: getWatchlist.

---

Outside diff comments:
In `@server/api/plextv.ts`:
- Around line 375-445: The outer catch currently swallows all errors and returns
the "access-denied sentinel" (endCursor: undefined), which causes transient or
unrelated failures to be treated as “no access”; change the final catch so it
only maps explicit access-denied errors (e.g. e.response?.status === 401 ||
e.response?.status === 403) to the sentinel and otherwise rethrows the error (or
preserve the cached endCursor via
cachedWatchlist?.response.data?.user.watchlist.pageInfo.endCursor) so transient
failures are not misclassified; update the catch block that currently logs via
logger.error('Failed to retrieve watchlist items', ...) and returns { first,
after, endCursor: undefined, ... } to perform the status check and rethrow for
non-access errors.

---

Nitpick comments:
In `@docs/using-seerr/plex/watchlist-auto-request.md`:
- Around line 63-65: Change the warning callout title from "Watchlist Not
Accessible" to the more concise "Watchlist Inaccessible" and replace the vague
phrase "you have not fulfilled prerequisite `#1`" with a clearer explanation such
as "your watchlist is not accessible to Seerr" or "you have not satisfied any of
the watchlist access options listed in the first prerequisite" so users
immediately understand the cause and next steps; update the callout content in
the watchlist warning block accordingly (look for the exact strings "Watchlist
Not Accessible" and "prerequisite `#1`" to locate the text).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cc59099-6791-4597-80ae-dca20fbedb77

📥 Commits

Reviewing files that changed from the base of the PR and between 27cbe73 and 90c712d.

📒 Files selected for processing (8)
  • docs/using-seerr/plex/watchlist-auto-request.md
  • server/api/plextv.ts
  • server/lib/watchlistsync.ts
  • server/routes/auth.ts
  • server/routes/user/index.ts
  • server/routes/user/usersettings.ts
  • server/utils/seedTestDb.ts
  • src/i18n/locale/en.json
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/locale/en.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/routes/user/usersettings.ts
  • server/routes/user/index.ts
  • server/lib/watchlistsync.ts

Comment thread server/api/plextv.ts
@xNul
Copy link
Copy Markdown
Author

xNul commented May 10, 2026

For those 8 outside diff range + nitpick comments:

  1. Fixed
  2. See "The Pagination Issue" in the description
  3. Fixed
  4. I guess I could add an accessDenied boolean return value? Pretty janky that way too.
  5. Fixed
  6. Fixed
  7. This is just #⁣4
  8. Fixed

Which just leaves pagination and maybe #⁣4.

Comment thread server/api/plextv.ts Dismissed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Fetch Plex user watchlists without needing their Plex user token

3 participants